#include <AceTime.h> //时区转换库（参考：https://www.arduino.cc/reference/en/libraries/acetime/）
#include <Adafruit_GPS.h> //GPS库（参考：https://www.arduino.cc/reference/en/libraries/adafruit-gps-library/）

//=========参数设置部分，可按需调整=========
//串口选择（也可选用软件串口）
#define DebugSerial Serial //调试输出
#define OutputSerial Serial1 //显示屏
//下面的OutputSerial.print或println代码是向显示屏模块发送指令，若使用其他种类显示屏则须修改这些代码
#define GPSSerial Serial2 //GPS

//调试输出波特率（由于GPS输出的数据量较大，不要使用低于GPSRATE的值，以防丢失字符）
#define DEBUGRATE 38400
//屏幕输出波特率（应与屏幕资源文件中设置的值保持一致）
#define OUTPUTRATE 38400
//GPS模块波特率（建议使用默认值，很多GPS模块长时间掉电之后会丢失设置，
//这样即使手动更改了模块的波特率设置也会回到默认值，从而与Arduino端的设置不匹配，造成通讯问题）
#define GPSRATE 9600

//时区选择（此处选择北京时间，要选择其他时区参考https://www.iana.org/time-zones给出的时区数据库）
#define localTimeZoneData kZoneAsia_Shanghai 

//农历显示开关，注释掉该行即可关闭
#define LUNARDISP

//注释掉以关闭NTP服务器功能
#define NTPENABLE

#ifdef NTPENABLE
//此处所有地址的分隔符都是逗号
//以太网MAC地址（若使用较新的以太网扩展板建议使用板上标明的MAC地址，没有的话则可以在不产生冲突的前提下随意设置）
#define macAddr 0x0C,0xA6,0xB3,0xCA,0x7E,0x32
//以太网IP地址（设置时应保证不会与其他设备冲突，建议与客户端设备处于同一网段并避开DHCP地址池）
#define ipAddr 192,168,0,21 
#define NTPPort 123 //NTP使用的123端口，一般来说不要修改
//以太网启动之后尝试连接该地址来测试以太网连接，若不成功则在屏幕上提示
//可以使用任一个正常情况下能访问到，并通过testPort指定的端口提供服务的IP地址
//一般情况下，IP地址用家里的路由器，端口为80（HTTP）即可
//如果每次开机总是出现网络报错，请先检查该地址设置是否正确，再排查IP地址是否有冲突以及其他网络问题
#define testAddr 192,168,0,1 
#define testPort 80 
#endif

//到串口监视器的调试输出，取消相应#define行开头的注释即可打开（非必要情况不建议同时打开多个，那样输出的信息会比较杂乱）
//#define GPSECHO //输出GPS原始NMEA语句
//#define HOLDOVERECHO //输出从上一次获取GPS时间数据（secondsSinceTime）及上一次GPS锁定信号（secondsSinceFix）开始至今过去的时间
//#define COUNTERECHO //每个loop周期输出millis()计时器的值（其实个人觉得没用）
#ifdef NTPENABLE
//#define NTPECHO //通过串口输出检查NTP服务器是否正确动作
#endif

//=========参数设置部分结束=========

#include "ChineseStringResA.h" //ATFLet显示屏只接受GB2312编码，而Arduino IDE只支持Unicode，
//故中文字符串需要用其他编辑器写在单独的文件中并保存为GB2312。如使用其他种类显示屏则请参照其文档

#ifdef LUNARDISP
#include "Solar2Lunar.h" //公农历与节气转换库
#include "ChineseStringResB.h" //用于农历显示的字符串资源
#endif

#ifdef NTPENABLE
#include <SPI.h> //通过SPI总线连接以太网扩展板
#include <Ethernet.h> //用于以太网连接（参考：https://www.arduino.cc/en/Reference/Ethernet）
#include <EthernetUdp.h> //用于通过UDP收发数据包
#include "ChineseStringResC.h" //NTP服务相关屏幕显示的字符串资源
#endif

Adafruit_GPS GPS(&GPSSerial); //GPS实例初始化

//时区转换实例初始化
using namespace ace_time;
static BasicZoneProcessor localProcessor;
auto localTz = TimeZone::forZoneInfo(&zonedb::localTimeZoneData,&localProcessor); 
TimeOffset utcOffset;
auto utcTime = OffsetDateTime::forComponents(2000, 1, 1, 0, 0, 0, utcOffset); 

#ifdef NTPENABLE
EthernetUDP Udp; //UDP实例初始化
#endif

//标志变量
char lastSecond = -1; //用于检测接收到的秒是否发生变化，如变化则更新时间显示
char lastDay = -1; //用于检测天（本地时间）是否发生变化，如变化则更新日期显示
bool timingReady = false; //表示GPS时间数据是否有
bool fixingReady = false; //表示GPS是否信号锁定

char printBuffer[50]; //待上屏文本的缓冲区
char c; //用于读取GPS数据
#ifdef NTPENABLE
char* packetBuffer; //NTP数据包缓冲区的地址
IPAddress remote; //用于记录客户端IP地址
bool replyPending = false; //表示是否有待回复的请求
unsigned short replyCounter = 0; //记录完成NTP响应的次数
#endif

void setup() {
  //外围设备I/O初始化
#if defined(GPSECHO) || defined(OFFSETECHO) || defined(COUNTERECHO) || defined(NTPECHO)
  DebugSerial.begin(DEBUGRATE); //调试串口
#endif
  OutputSerial.begin(OUTPUTRATE); //显示屏
  GPS.begin(GPSRATE); //GPS模块
#ifdef NTPENABLE
  Ethernet.init(10); //以太网扩展板，标准款的CS脚位为10，有的以太网板会使用其他脚位
#endif
  delay(1000);

  //显示初始画面，详见ATFLet系列触摸屏模块说明文档
  OutputSerial.println(F("@PRINTM 0")); //关字符串直接打印模式；指令内容通过F()存储在Flash中，下同
  delay(20);
  OutputSerial.println(F("@BKL 200")); //显示屏亮度（0~500）

  //设置GPS模块参数（此处根据中科微GPS模块调试软件生成的串口消息，如使用其他品牌的模块请参考其文档）    
  //仅启用NMEA RMC语句，关闭其他语句输出，消除不必要的语句解析操作引起的性能浪费
  GPSSerial.println(F("$PCAS03,0,0,0,0,1,0,0,0,0,0,,,0,0*03"));
  delay(20);
  GPSSerial.println(F("$PCAS02,100*1E")); //设置定位更新率10Hz，以更准确地计时

#ifdef NTPENABLE
  //启动以太网连接
  byte myMAC[] = {macAddr};
  byte myIP[] = {ipAddr};
  Ethernet.begin(myMAC,myIP);
  
  //测试网络连接
  EthernetClient client; IPAddress serverIP(testAddr); 
  if (client.connect(serverIP, testPort)) { //连接成功，显示本机IP地址
    OutputSerial.print(F("@SET 112 "));
    strcpy_P(printBuffer, (char *)pgm_read_word(&(strNetwork[1]))); OutputSerial.print(printBuffer);
    OutputSerial.println(Ethernet.localIP());
#ifdef NTPECHO
    DebugSerial.print("Connected to "); DebugSerial.println(client.remoteIP()); 
#endif
  }
  else { //连接失败，显示报错
    OutputSerial.print(F("@SET 112 "));
    strcpy_P(printBuffer, (char *)pgm_read_word(&(strNetwork[0]))); OutputSerial.println(printBuffer);
#ifdef NTPECHO
    DebugSerial.println("Connection failed");
#endif
  }
  Udp.begin(NTPPort);
#endif
}

void loop() {
  TimingActions(); //接收GPS时间并更新时钟

  if (GPS.secondsSinceTime() < 2 && !timingReady) 
    { OutputSerial.println(F("@GUIS 1")); timingReady = true; }  //GPS时间数据准备好时切换到时钟画面
  if (GPS.secondsSinceTime() > 2 && timingReady) 
    { OutputSerial.println(F("@GUIS 2")); timingReady = false; }  //GPS时间数据丢失后切换到报错画面

  if (GPS.secondsSinceFix() < 3 && !fixingReady) fixingReady = true; //GPS信号锁定时置标志位，消除时间显示上的警告
  if (GPS.secondsSinceFix() > 10 && fixingReady) fixingReady = false; //GPS信号丢失时清标志位，在时间显示上显示警告
 
#ifdef COUNTERECHO
  DebugSerial.println(millis());
#endif

#ifdef NTPENABLE
  int packetSize = Udp.parsePacket();
  if (packetSize >= 48 && !replyPending) { 
    remote = Udp.remoteIP(); //有NTP请求时记录来源IP
    
    packetBuffer = new char[48];
    Udp.read(packetBuffer, 48);
    WriteNTPHeader(packetBuffer); //写入NTP标头
    replyPending = true;
    
#ifdef NTPECHO
    DebugSerial.print("Request from ");
    for (int i=0; i < 4; i++) {
      DebugSerial.print(remote[i], DEC);
      if (i < 3) DebugSerial.print(".");
    }
    DebugSerial.println("");
#endif
  }

  if (replyPending && !timingReady) { //当GPS时间丢失时，置LI=3（参考时钟异常），然后将包发回
    packetBuffer[0] = 0b11011100; 
      
    Udp.beginPacket(remote, NTPPort); Udp.write(packetBuffer,48); Udp.endPacket();

    delete [] packetBuffer;
    replyPending = false;
        
#ifdef NTPECHO
    DebugSerial.print("Reply ERR to ");
    for (int i=0; i < 4; i++) {
      DebugSerial.print(remote[i], DEC);
      if (i < 3) DebugSerial.print(".");
    }
    DebugSerial.println("");
#endif
  }
#endif
}

void TimingActions() { //GPS时间的获取
  c = GPS.read(); //从GPS读数据  
  // 调试输出
#ifdef GPSECHO
  if (c)
    DebugSerial.print(c);
#endif
#ifdef HOLDOVERECHO
  if ((unsigned long)(millis() / 20) % 50 == 0) {
    DebugSerial.print(GPS.secondsSinceTime(), 3);
    DebugSerial.print(' ');
    DebugSerial.println(GPS.secondsSinceFix(), 3);
  }
#endif
  if (GPS.newNMEAreceived()) { //当收到NMEA语句时开始更新时间
    if (!GPS.parse(GPS.lastNMEA())) //解析收到的NMEA语句
      return; //若解析不成功则放弃，退出TimingActions()

    //获取时间数据
    long secCorr = GPS.seconds + GPS.secondsSinceTime();
    int minCorr = GPS.minute;
    int hourCorr = GPS.hour;
    byte dayCorr = GPS.day;
    byte monthCorr = GPS.month;
    byte tyearCorr = GPS.year;

    /*若距离上次获取到时间数据较久，GPS.secondsSinceTime()值较大导致
     GPS.seconds + GPS.secondsSinceTime() >= 60，就需要进位以保证时间显示的正确性。
     日期进位未作考虑，若在新的一月来临时发生GPS丢失，则显示屏及NTP会出现错误的日期。
     不过，丢失满10秒后就会报错并停止NTP服务，直到GPS恢复后才会自动恢复正常*/
    while (secCorr >= 60) {
      secCorr -= 60;
      minCorr++;
    }
    while (minCorr >= 60) {
      minCorr -= 60;
      hourCorr++;
    }
    while (hourCorr >= 24) {
      hourCorr -= 24;
      dayCorr++;
    }
  
    if (secCorr != lastSecond){ //当新的一秒来临时才更新时间显示
      utcTime.yearTiny(tyearCorr);
      utcTime.month(monthCorr);
      utcTime.day(dayCorr);
      utcTime.hour(hourCorr);
      utcTime.minute(minCorr);
      utcTime.second(secCorr); 
      unsigned long aceEpoch = utcTime.toEpochSeconds(); //从AceTime库的epoch至今的秒数

#ifdef NTPENABLE
      if (replyPending) { //有请求待处理则处理
        unsigned long ntpEpoch = aceEpoch + 36524*24*3600; //定点时间戳整数部分，从NTP时间戳epoch至今的秒数（小端序）
        /*疑问：AceTime库的epoch是2000.1.1 00:00:00（https://bxparks.github.io/AceTime/html/classace__time_1_1OffsetDateTime.html），
        NTP时间戳的epoch则是1900.1.1 00:00:00（https://www.ietf.org/rfc/rfc1769.txt）。
        按说两者应该相差365*100+25=36525天，但用这个数计算出的NTP epoch seconds比实际NTP包里的值
        多出了86400秒（一天），按36524天来算就正确了，但我也搞不清楚为什么要少一天*/
        unsigned long frac = millis2FracStamp(GPS.milliseconds); //由毫秒数转换来的定点时间戳小数部分（小端序）
        WriteNTPTimestamps(packetBuffer, ntpEpoch, frac);
        
        Udp.beginPacket(remote, NTPPort); Udp.write(packetBuffer,48); Udp.endPacket();

        delete [] packetBuffer; 
        replyPending = false;

        replyCounter++;
        OutputSerial.print(F("@SET 111 "));
        strcpy_P(printBuffer, (char *)pgm_read_word(&(strNetwork[2]))); OutputSerial.print(printBuffer);
        OutputSerial.print(replyCounter, DEC);
        strcpy_P(printBuffer, (char *)pgm_read_word(&(strNetwork[3]))); OutputSerial.println(printBuffer);
        
#ifdef NTPECHO
        DebugSerial.print("Reply to ");
        for (int i=0; i < 4; i++) {
          DebugSerial.print(remote[i], DEC);
          if (i < 3) DebugSerial.print(".");
        }
        DebugSerial.println("");
#endif
      }
#endif

      auto myTime = ZonedDateTime::forEpochSeconds(aceEpoch, localTz); //将GPS提供的UTC时间转换为当地时间
      UpdateTimeDisp(myTime.hour(), myTime.minute(), myTime.second());
      if (myTime.day() != lastDay) { //当新的一天来临时才更新日期显示
        UpdateDateDisp(myTime.year(), myTime.month(), myTime.day(), myTime.dayOfWeek());
        lastDay = myTime.day();
      }
        
      lastSecond = secCorr;
    }
  }
}
  
void UpdateTimeDisp(unsigned int hr, unsigned int mn, unsigned int sec) { // 更新时间显示
  OutputSerial.print(F("@SET 103 "));
  if (!fixingReady) OutputSerial.print('*'); //当GPS信号失锁时在时间显示两侧标星号以示警告
  if (hr < 10) OutputSerial.print('0'); 
  OutputSerial.print(hr, DEC); OutputSerial.print(':');
  if (mn < 10) OutputSerial.print('0'); 
  OutputSerial.print(mn, DEC); OutputSerial.print(':');
  if (sec < 10) OutputSerial.print('0'); 
  OutputSerial.print(sec, DEC);
  if (!fixingReady) OutputSerial.println('*'); else OutputSerial.println("");
}

void UpdateDateDisp(int yyyy, unsigned int mm, unsigned int dd, unsigned int weekday) { // 更新日期显示（含公农历转换）
  //公历日期显示
  delay(40); //此处不延时则日期显示不出来
  OutputSerial.print(F("@SET 104 "));
  OutputSerial.print(yyyy, DEC); 
  strcpy_P(printBuffer, (char *)pgm_read_word(&(singleStrings[0])));  OutputSerial.print(printBuffer); //“年”字
  OutputSerial.print(mm, DEC);  
  strcpy_P(printBuffer, (char *)pgm_read_word(&(singleStrings[1])));  OutputSerial.print(printBuffer); //“月”字
  OutputSerial.print(dd, DEC); 
  strcpy_P(printBuffer, (char *)pgm_read_word(&(singleStrings[2])));  OutputSerial.print(printBuffer); //“日”字
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strWeekNumber[weekday-1])));  OutputSerial.println(printBuffer); //星期

#ifdef LUNARDISP
  cnDate lunarResult = Solar2Lunar(yyyy, mm, dd); //公历转农历
  unsigned char termResult = Solar2Term(yyyy, mm, dd); //查询节气
  //农历日期显示
  delay(40);
  OutputSerial.print(F("@SET 102 ")); 
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strTiangan[lunarResult.tiangan])));  OutputSerial.print(printBuffer); //农历年天干
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strDizhi[lunarResult.dizhi])));  OutputSerial.print(printBuffer); //农历年地支
  if (lunarResult.isLeapMonth) 
    { strcpy_P(printBuffer, (char *)pgm_read_word(&(singleStrings[3])));  OutputSerial.print(printBuffer); } //如果是闰月则显示“闰”字
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strLunarMonth[lunarResult.lunarMonth - 1])));  OutputSerial.print(printBuffer); //农历月
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strLunarDay[lunarResult.lunarDay - 1])));  OutputSerial.print(printBuffer); //农历日
  strcpy_P(printBuffer, (char *)pgm_read_word(&(strSolarTerms[termResult])));  OutputSerial.println(printBuffer); //节气
#endif
}

#ifdef NTPENABLE
void WriteNTPHeader(char* bufferPtr) { //写入NTP标头，参数为指向包缓冲区的指针
  bufferPtr[0] = 0b00011100; //LI=0（正常）；VN=3；Mode=4（服务器模式）
  bufferPtr[1] = 1; //Stratum 1服务器；Polling Interval直接使用请求中的值
  //本地时钟精度2^-5；Root Delay与Root Dispersion不用，置零；参考时钟为GPS
  const char predefinedHeader[] = { -5,0,0,0,0,0,0,0,0,'G','P','S',0 };
  memcpy(&bufferPtr[3],predefinedHeader,13); //写入标头的其他部分
}

void WriteNTPTimestamps(char* bufferPtr,unsigned long epoch,unsigned long frac) { 
//写入NTP时间戳，参数为指向包缓冲区的指针、定点数时间戳的整数和小数部分（小端序，下面会转换成大端序）
  unsigned char* epochBytes = (unsigned char*)&epoch;
  unsigned char* fracBytes = (unsigned char*)&frac; //整数与小数部分分别拆成4字节
  unsigned char finalTimestamp[8]; //最终写入响应包的定点数时间戳。下面进行字节序转换，整数与小数部分分别由小端序转大端序写入时间戳
  finalTimestamp[0] = epochBytes[3]; finalTimestamp[1] = epochBytes[2]; finalTimestamp[2] = epochBytes[1]; finalTimestamp[3] = epochBytes[0]; 
  finalTimestamp[4] = fracBytes[3]; finalTimestamp[5] = fracBytes[2]; finalTimestamp[6] = fracBytes[1]; finalTimestamp[7] = fracBytes[0];

  //Reference Timestamp
  memcpy(&bufferPtr[16], finalTimestamp, 8);
  //Originate Timestamp（复制自请求的Transmit Timestamp）
  memcpy(&bufferPtr[24], &bufferPtr[40], 8);
  //Receive Timestamp
  memcpy(&bufferPtr[32], finalTimestamp, 8);
  //Transmit Timestamp
  memcpy(&bufferPtr[40], finalTimestamp, 8);
}

//将GPS报告的整型毫秒数转换成定点数时间戳中的小数部分
unsigned long millis2FracStamp(unsigned short milliSecs){ 
  unsigned short residue = milliSecs;
  unsigned long fracStamp = 0; 
  unsigned long currDigit;
  
  for(int i=0; i<2*sizeof(fracStamp); i++){ 
    currDigit = residue*16/1000;
    fracStamp = fracStamp | (currDigit << (7-i)*4); 
    residue = residue*16 - currDigit*1000; 
    if (residue == 0) break;
  }

  return fracStamp;
}
#endif
